﻿namespace Blaze.FileSystems
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Runtime.InteropServices;
    using Utilities;

    public class PkgFile : IDisposable
    {
        public struct HashTableItem
        {
            /// <summary>
            /// 文件路径所对应的哈希值。
            /// </summary>
            public ulong Hash;

            /// <summary>
            /// 指定该哈希值所对应的数据块索引。
            /// </summary>
            public int BlockIndex;

            public void Write(BinaryWriter writer)
            {
                writer.Write(Hash);
                writer.Write(BlockIndex);
            }

            public static HashTableItem Read(BinaryReader reader)
            {
                var ret = new HashTableItem
                {
                    Hash = reader.ReadUInt64(),
                    BlockIndex = reader.ReadInt32()
                };
                return ret;
            }
        }

        public struct BlockTableItem
        {
            /// <summary>
            /// 数据块所在的位置。
            /// </summary>
            public int Position;

            /// <summary>
            /// 数据块大小。
            /// </summary>
            public int Size;

            public void Write(BinaryWriter writer)
            {
                writer.Write(Position);
                writer.Write(Size);
            }

            public static BlockTableItem Read(BinaryReader reader)
            {
                var ret = new BlockTableItem
                {
                    Position = reader.ReadInt32(),
                    Size = reader.ReadInt32(),
                };
                return ret;
            }
        }

        public struct FileHeader
        {
            /// <summary>
            /// 固定头，PKG
            /// </summary>
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 3)]
            public string Magic;

            /// <summary>
            /// 版本号。
            /// </summary>
            public byte Version;

            /// <summary>
            /// 哈希表位置。
            /// </summary>
            public int HashTablePosition;

            /// <summary>
            /// 哈希表行数。
            /// </summary>
            public int HashTableItemCount;

            /// <summary>
            /// 数据块索引表位置。
            /// </summary>
            public int BlockTablePosition;

            /// <summary>
            /// 数据块索引表行数。
            /// </summary>
            public int BlockTableItemCount;

            public static FileHeader Read(BinaryReader reader)
            {
                var ret = new FileHeader
                {
                    Magic = new string(reader.ReadChars(3)),
                    Version = reader.ReadByte(),
                    HashTablePosition = reader.ReadInt32(),
                    HashTableItemCount = reader.ReadInt32(),
                    BlockTablePosition = reader.ReadInt32(),
                    BlockTableItemCount = reader.ReadInt32()
                };
                return ret;
            }

            public void Write(BinaryWriter writer)
            {
                writer.Write(Magic.ToCharArray());
                writer.Write(Version);
                writer.Write(HashTablePosition);
                writer.Write(HashTableItemCount);
                writer.Write(BlockTablePosition);
                writer.Write(BlockTableItemCount);
            }
        }

        public IEnumerable<ulong> Hashes => mHashToBlock.Keys;

        #region IDisposable Members

        public void Dispose()
        {
            Close();
        }

        #endregion

        private PkgFile(Stream input, int bufferSize)
        {
            mBuffer = new byte[bufferSize];
            mStream = input;
            var reader = new BinaryReader(input);
            var header = FileHeader.Read(reader);
            if (header.Magic != "PKG")
                throw new Exception("invalid magic header -> " + header.Magic);
            mHashToBlock = new Dictionary<ulong, BlockTableItem>(header.HashTableItemCount);

            mStream.Position = header.BlockTablePosition;
            var blockTableItems = new BlockTableItem[header.BlockTableItemCount];
            for (var i = 0; i < header.BlockTableItemCount; i++)
            {
                var item = BlockTableItem.Read(reader);
                blockTableItems[i] = item;
            }

            mStream.Position = header.HashTablePosition;
            for (var i = 0; i < header.HashTableItemCount; i++)
            {
                var item = HashTableItem.Read(reader);
                mHashToBlock[item.Hash] = blockTableItems[item.BlockIndex];
            }
        }

        public static PkgFile Read(Stream input, int bufferSize = 40960)
        {
            if (input == null)
                throw new ArgumentNullException(nameof(input));
            var ret = new PkgFile(input, bufferSize);
            return ret;
        }

        public void Close()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public bool Exists(ulong hash)
        {
            return mHashToBlock.ContainsKey(hash);
        }

        public MemoryStream Read(string path)
        {
            var hash = XXHash64.Hash(path);
            return Read(hash);
        }

        public MemoryStream Read(ulong hash)
        {
            if (!mHashToBlock.TryGetValue(hash, out var block))
                return null;
            mStream.Position = block.Position;
            var ret = new MemoryStream(block.Size);
            var remain = block.Size;
            while (remain > 0)
            {
                var count = mStream.Read(mBuffer, 0, Math.Min(remain, mBuffer.Length));
                ret.Write(mBuffer, 0, count);
                remain -= count;
            }
            ret.Position = 0;
            return ret;
        }

        protected void Dispose(bool disposeManagedResources)
        {
            if (mIsDisposed)
                return;
            if (disposeManagedResources)
                mStream?.Dispose();
            mIsDisposed = true;
        }

        ~PkgFile()
        {
            Dispose(false);
        }

        private readonly Stream mStream;
        private bool mIsDisposed;
        private readonly Dictionary<ulong, BlockTableItem> mHashToBlock;
        private readonly byte[] mBuffer;
    }
}